package com.katesoft.scale4j.persistent.client;

import com.katesoft.scale4j.persistent.model.unified.AbstractPersistentEntity;
import com.katesoft.scale4j.persistent.model.unified.BO;
import com.katesoft.scale4j.persistent.model.unified.IBO;
import net.jcip.annotations.ThreadSafe;
import org.hibernate.Criteria;
import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.StaleObjectStateException;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Restrictions;
import org.hibernate.metadata.ClassMetadata;
import org.perf4j.aop.Profiled;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException;
import org.springframework.orm.hibernate3.HibernateTemplate;

import java.sql.SQLException;
import java.util.List;
import java.util.Map.Entry;

import static org.hibernate.criterion.Restrictions.eq;

/**
 * Default implementation of IHibernateOperations interface.
 * <p/>
 * find methods marked with Profiled annotation, so runtime performance static can be gathered. jvmcluster-persistent does not actually track performance
 * statistic, but jvmcluster-rttp contains performance aspect, so execution of methods are tracked by pef4j framework.
 * <p/>
 * NOTE:
 * <p/>
 * 1) exposeNativeSession flag is always true, user overrides are suppressed(method for setting this property is not final, you can override if needed).
 * <p/>
 * 2) allowCreate flag is always true, user overrides are suppressed(method for setting this property is not final, you can override if needed).
 * <p/>
 * 3) checkWriteOperations flag is always false, user overrides are suppressed(method for setting this property is not final, you can override if needed).
 * <p/>
 * 4) alwaysUseNewSession flag is always false, user overrides are suppressed(method for setting this property is not final, you can override if needed).
 *
 * @author kate2007
 */
@ThreadSafe
public class LocalHibernateTemplate extends HibernateTemplate implements IHibernateOperations
{
    public LocalHibernateTemplate()
    {
        super();
        super.setExposeNativeSession(true);
        super.setAllowCreate(true);
        super.setCheckWriteOperations(false);
        super.setAlwaysUseNewSession(false);
    }

    public LocalHibernateTemplate(final SessionFactory sessionFactory)
    {
        this();
        super.setSessionFactory(sessionFactory);
    }

    @Override
    public <T extends AbstractPersistentEntity> T findEntityForUpdate(final Class<T> entityClass,
                                                                      final long uniqueIdentifier,
                                                                      long expectedVersion) throws DataAccessException
    {
        T result = execute(new HibernateCallback<T>()
        {
            @SuppressWarnings({"unchecked"})
            @Override
            public T doInHibernate(Session session) throws HibernateException, SQLException
            {
                Criteria criteria = session.createCriteria(entityClass);
                criteria.add(Restrictions.eq(BO.PROP_UID, uniqueIdentifier));
                return (T) criteria.uniqueResult();
            }
        });
        if (result == null) {
            throw new EmptyResultDataAccessException(String.format("unable to find %s using unique_identifier %s", entityClass, uniqueIdentifier), 1);
        }
        if (result.getVersion() != expectedVersion) {
            throw new HibernateOptimisticLockingFailureException(new StaleObjectStateException(entityClass.getName(), uniqueIdentifier));
        }
        return result;
    }

    @Override
    @Profiled(tag = "hibernate.loadAll_{$0.name}", message = "loadAll called with parameters[class={$0.name} forceAttributesLoad={$1}]")
    public <T extends AbstractPersistentEntity> List<T> loadAll(final Class<T> entityClass,
                                                                final boolean forceLoad)
    {
        return execute(new HibernateCallback<List<T>>()
        {
            @SuppressWarnings({"unchecked"})
            @Override
            public List<T> doInHibernate(Session session) throws HibernateException, SQLException
            {
                List<T> list = session.createCriteria(entityClass).list();
                for (T next : list) {
                    next.forceAttributesLoad();
                }
                return list;
            }
        });
    }

    @Override
    protected void prepareQuery(final Query queryObject)
    {
        super.prepareQuery(queryObject);
    }

    @Override
    protected void prepareCriteria(final Criteria criteria)
    {
        super.prepareCriteria(criteria);
    }

    @Override
    @Profiled(tag = "hibernate.flush")
    public void flush() throws DataAccessException
    {
        super.flush();
    }

    @Override
    @Profiled(tag = "hibernate.loadAll_{$0.name}", message = "loadAll called with parameters[class={$0.name}]")
    public <T> List<T> loadAll(Class<T> entityClass) throws DataAccessException
    {
        return super.loadAll(entityClass);
    }

    @Override
    @Profiled(tag = "hibernate.deleteAll_{$0.name}", message = "deleteAll called with parameters[class={$0.name}]")
    public void deleteAll(final Class<?> clazz)
    {
        execute(new HibernateCallback<Object>()
        {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException
            {
                for (Object next : session.createCriteria(clazz).list()) {
                    session.delete(next);
                }
                session.flush();
                return null;
            }
        });
    }

    public <T extends AbstractPersistentEntity> T loadByGlobalUniqueIdentifier(final Class<T> persistentClazz,
                                                                               final String guid,
                                                                               final boolean forceDirtyLoad)
    {
        return execute(new HibernateCallback<T>()
        {
            @SuppressWarnings({"unchecked"})
            @Override
            public T doInHibernate(Session session) throws HibernateException, SQLException
            {
                T t = (T) session.createCriteria(persistentClazz).add(eq(BO.PROP_GUID, guid)).uniqueResult();
                if (forceDirtyLoad && t != null) { t.forceAttributesLoad(); }
                return t;
            }
        });
    }

    @SuppressWarnings({"unchecked"})
    public <T extends AbstractPersistentEntity> T loadByGlobalUniqueIdentifier(final String guid,
                                                                               final boolean forceDirtyLoad)
    {
        for (Entry<String, ClassMetadata> entry : getSessionFactory().getAllClassMetadata().entrySet()) {
            Class clazz = entry.getValue().getMappedClass(EntityMode.POJO);
            if (IBO.class.isAssignableFrom(clazz)) {
                AbstractPersistentEntity candidate = loadByGlobalUniqueIdentifier(clazz, guid, forceDirtyLoad);
                if (candidate != null) {
                    return (T) candidate;
                }
            }
        }
        return null;
    }

    @Override
    @Profiled(tag = "hibernate.findByNamedParam_{$0}",
              message = "findByNamedParam called with parameters[queryString={$0} paramName={$1} value={$2}] and returned {$return.size} elements")
    public List findByNamedParam(String queryString,
                                 String paramName,
                                 Object value) throws DataAccessException
    {
        return super.findByNamedParam(queryString, paramName, value);
    }

    @Override
    @Profiled(tag = "hibernate.findByNamedParam_{$0}",
              message = "findByNamedParam called with parameters[queryString={$0} paramNames={$1} values={$2}] and returned {$return.size} " + "elements")
    public List findByNamedParam(String queryString,
                                 String[] paramNames,
                                 Object[] values) throws DataAccessException
    {
        return super.findByNamedParam(queryString, paramNames, values);
    }

    @Override
    @Profiled(tag = "hibernate.findByValueBean_{$0}",
              message = "findByValueBean called with parameters[queryString={$0} valueBean={$1}] and returned {$return.size} elements")
    public List findByValueBean(String queryString,
                                Object valueBean) throws DataAccessException
    {
        return super.findByValueBean(queryString, valueBean);
    }

    @Override
    @Profiled(tag = "hibernate.findByNamedQuery_{$0}",
              message = "findByNamedQuery called with parameters[queryName={$0}] and returned {$return.size} elements")
    public List findByNamedQuery(String queryName) throws DataAccessException
    {
        return super.findByNamedQuery(queryName);
    }

    @Override
    @Profiled(tag = "jvmcluster.hibernate.findByNamedQuery_{$0}",
              message = "findByNamedQuery called with parameters[queryName={$0} value={$1}] and returned {$return.size} elements")
    public List findByNamedQuery(String queryName,
                                 Object value) throws DataAccessException
    {
        return super.findByNamedQuery(queryName, value);
    }

    @Override
    @Profiled(tag = "hibernate.findByNamedQuery_{$0}",
              message = "findByNamedQuery called with parameters[queryName={$0} values={$1}] and returned {$return.size} elements")
    public List findByNamedQuery(String queryName,
                                 Object... values) throws DataAccessException
    {
        return super.findByNamedQuery(queryName, values);
    }

    @Override
    @Profiled(tag = "hibernate.findByNamedQueryAndNamedParam_{$0}",
              message = "findByNamedQueryAndNamedParam called with parameters[queryName={$0} paramName={$1} value={$2}] and returned {$return.size} " +
                        "elements")
    public List findByNamedQueryAndNamedParam(String queryName,
                                              String paramName,
                                              Object value) throws DataAccessException
    {
        return super.findByNamedQueryAndNamedParam(queryName, paramName, value);
    }

    @Override
    @Profiled(tag = "hibernate.findByNamedQueryAndNamedParam_{$0}",
              message = "findByNamedQueryAndNamedParam called with parameters[queryName={$0} paramNames={$1} values={$2}] and returned {$return.size} " +
                        "elements")
    public List findByNamedQueryAndNamedParam(String queryName,
                                              String[] paramNames,
                                              Object[] values) throws DataAccessException
    {
        return super.findByNamedQueryAndNamedParam(queryName, paramNames, values);
    }

    @Override
    @Profiled(tag = "hibernate.findByNamedQueryAndValueBean_{$0}",
              message = "findByNamedQueryAndValueBean called with parameters[queryName={$0} valueBean={$1}] and returned {$return.size} elements")
    public List findByNamedQueryAndValueBean(String queryName,
                                             Object valueBean) throws DataAccessException
    {
        return super.findByNamedQueryAndValueBean(queryName, valueBean);
    }

    @Override
    @Profiled(tag = "hibernate.findByCriteria",
              message = "findByCriteria returned {$return.size} elements")
    public List findByCriteria(DetachedCriteria criteria) throws DataAccessException
    {
        return super.findByCriteria(criteria);
    }

    @Override
    @Profiled(tag = "hibernate.findByCriteria",
              message = "findByCriteria called with parameters[criteria={$0} firstResult={$1} maxResults={$2}] and returned {$return.size} " + "elements")
    public List findByCriteria(DetachedCriteria criteria,
                               int firstResult,
                               int maxResults) throws DataAccessException
    {
        return super.findByCriteria(criteria, firstResult, maxResults);
    }

    @Override
    @Profiled(tag = "hibernate.findByExample",
              message = "findByExample called with parameters[exampleEntity={$0}] and returned {$return.size} elements")
    public List findByExample(Object exampleEntity) throws DataAccessException
    {
        return super.findByExample(exampleEntity);
    }

    @Override
    @Profiled(tag = "hibernate.findByExample",
              message = "findByExample called with parameters[exampleEntity={$0} firstResult={$1} maxResults={$2}] and returned {$return.size} elements")
    public List findByExample(Object exampleEntity,
                              int firstResult,
                              int maxResults) throws DataAccessException
    {
        return super.findByExample(exampleEntity, firstResult, maxResults);
    }

    @Override
    @Profiled(tag = "hibernate.bulkUpdate_{$0}",
              message = "bulkUpdate called with parameters[queryString={$0}] and rows affected {$return}")
    public int bulkUpdate(String queryString) throws DataAccessException
    {
        return super.bulkUpdate(queryString);
    }

    @Override
    @Profiled(tag = "hibernate.bulkUpdate_{$0}",
              message = "bulkUpdate called with parameters[queryString={$0} and value={$1}] and rows affected {$return}")
    public int bulkUpdate(String queryString,
                          Object value) throws DataAccessException
    {
        return super.bulkUpdate(queryString, value);
    }

    @Override
    @Profiled(tag = "hibernate.bulkUpdate_{$0}",
              message = "bulkUpdate called with parameters[queryString={$0} and values={$1}] and rows affected {$return}")
    public int bulkUpdate(String queryString,
                          Object... values) throws DataAccessException
    {
        return super.bulkUpdate(queryString, values);
    }
}
